R2 ์‚ฌ์šฉ

7/2/2025

s3 sdk

์•„๋งˆ์กด์˜ AWS S3 SDK๋ฅผ ์‚ฌ์šฉํ•ด์„œ R2 ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ https://developers.cloudflare.com/r2/examples/aws/aws-sdk-java/ ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ œ์‹œํ•œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์•˜๋‹ค.

์ฝ”๋“œ1 (๊ณต์‹)

/**
 * Client for interacting with Cloudflare R2 Storage using AWS SDK S3 compatibility
 */
public class CloudflareR2Client {
    private final S3Client s3Client;

    /**
     * Creates a new CloudflareR2Client with the provided configuration
     */
    public CloudflareR2Client(S3Config config) {
        this.s3Client = buildS3Client(config);
    }

    /**
     * Configuration class for R2 credentials and endpoint
     */
    public static class S3Config {
        private final String accountId;
        private final String accessKey;
        private final String secretKey;
        private final String endpoint;

        public S3Config(String accountId, String accessKey, String secretKey) {
            this.accountId = accountId;
            this.accessKey = accessKey;
            this.secretKey = secretKey;
            this.endpoint = String.format(
            "https://%s.r2.cloudflarestorage.com", accountId);
        }

        public String getAccessKey() { return accessKey; }
        public String getSecretKey() { return secretKey; }
        public String getEndpoint() { return endpoint; }
    }

    /**
     * Builds and configures the S3 client with R2-specific settings
     */
    private static S3Client buildS3Client(S3Config config) {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(
            config.getAccessKey(),
            config.getSecretKey()
        );

        S3Configuration serviceConfiguration = S3Configuration.builder()
            .pathStyleAccessEnabled(true)
            .build();

        return S3Client.builder()
            .endpointOverride(URI.create(config.getEndpoint()))
            .credentialsProvider(StaticCredentialsProvider.create(credentials))
            .region(Region.of("auto"))
            .serviceConfiguration(serviceConfiguration)
            .build();
    }

    /**
     * Lists all buckets in the R2 storage
     */
    public List<Bucket> listBuckets() {
        try {
            return s3Client.listBuckets().buckets();
        } catch (S3Exception e) {
            throw new RuntimeException(
            "Failed to list buckets: " + e.getMessage(), e);
        }
    }

    /**
     * Lists all objects in the specified bucket
     */
    public List<S3Object> listObjects(String bucketName) {
        try {
            ListObjectsV2Request request = ListObjectsV2Request.builder()
                .bucket(bucketName)
                .build();

            return s3Client.listObjectsV2(request).contents();
        } catch (S3Exception e) {
            throw new RuntimeException(
            "Failed to list objects in bucket "
            + bucketName + ": " + e.getMessage(), e);
        }
    }

    public static void main(String[] args) {
        S3Config config = new S3Config(
            "your_account_id",
            "your_access_key",
            "your_secret_key"
        );

        CloudflareR2Client r2Client = new CloudflareR2Client(config);

        // List buckets
        System.out.println("Available buckets:");
        r2Client.listBuckets().forEach(bucket ->
            System.out.println("* " + bucket.name())
        );

        // List objects in a specific bucket
        String bucketName = "demos";
        System.out.println("\nObjects in bucket '" + bucketName + "':");
        r2Client.listObjects(bucketName).forEach(object ->
            System.out.printf("* %s (size: %d bytes, modified: %s)%n",
                object.key(),
                object.size(),
                object.lastModified())
        );
    }
}

์ด๊ฑด ์ด์ œ ์ˆœ์ˆ˜ Java๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ์ด๊ณ  ์ด๊ฑธ ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์— ๋งž์ถฐ์„œ ์ž‘์„ฑํ•˜๋ฉด

์ฝ”๋“œ2 (์Šคํ”„๋ง)

@Component
public class CloudflareR2Client {
    private final S3Client s3Client;

    public CloudflareR2Client(S3Client s3Client){
        this.s3Client = s3Client;
    }

    public List<Bucket> listBuckets() {
        try {
            return s3Client.listBuckets().buckets();
        } catch (S3Exception e) {
            throw new RuntimeException(
	            "Failed to list buckets: " + e.getMessage(), e);
        }
    }

    public List<S3Object> listObjects(String bucketName) {
        try {
            ListObjectsV2Request request = ListObjectsV2Request.builder()
                    .bucket(bucketName)
                    .build();

            return s3Client.listObjectsV2(request).contents();
        } catch (S3Exception e) {
            throw new RuntimeException(
	            "Failed to list objects in bucket " 
	            + bucketName + ": " + e.getMessage(), e);
        }
    }
}
@Configuration
public class S3Config {
    @Value("${cloudflare.r2.account.id}")
    private String accountId;
    @Value("${cloudflare.r2.access.key}")
    private String accessKey;
    @Value("${cloudflare.r2.secret.key}")
    private String secretKey;

    @Bean
    public S3Client buildS3Client(){
        String endpoint = String.format(
        "https://%s.r2.cloudflarestorage.com", accountId);

        AwsBasicCredentials credentials = AwsBasicCredentials.create(
                accessKey, secretKey
        );

        S3Configuration serviceConfiguration = S3Configuration.builder()
                .pathStyleAccessEnabled(true)
                .build();

        return S3Client.builder()
                .endpointOverride(URI.create(endpoint))
                .credentialsProvider(
	                StaticCredentialsProvider.create(credentials))
                .region(Region.of("auto"))
                .serviceConfiguration(serviceConfiguration)
                .build();
    }
}

์‚ฌ์‹ค S3Client๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฑด๋ฐ ์™œ ์ด๋ ‡๊ฒŒ ํ•˜๋Š”๊ฑด์ง€๋Š” ๋ชจ๋ฅด๊ฒ ๋‹ค. ํŒŒ์ด๋„ ํ”„๋กœ์ ํŠธ์—์„œ ์ผ๋˜ s3config ์ฝ”๋“œ๋ž‘ ๊ฑฐ์˜ ํก์‚ฌ๋‹คํ•˜๋‹ค.

@Configuration
public class S3Config {
	
	@Value("${cloud.aws.credentials.access-key}")
	private String accessKey;
	
	@Value("${cloud.aws.credentials.secret-key}")
	private String secretKey;
	
	@Value("${cloud.aws.region.static}")
	private String region;
	
	@Bean
	public AmazonS3Client amazonS3Client() {
		// AWS ์ธ์ฆ ์ •๋ณด (Access Key, Secret Key) ์„ค์ •
		BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
		
		return (AmazonS3Client) AmazonS3ClientBuilder
					.standard() // ๊ธฐ๋ณธ์„ค์ •
					.withRegion(region)
					.withCredentials(
					new AWSStaticCredentialsProvider(credentials))
					.build();
	}
}

๋ฌธ์ œ์ 

๋ž˜ํผ ๋ฉ”์†Œ๋“œ๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

 public void uploadImage(String bucket, String key, byte[] data){
        try{
            PutObjectRequest request = PutObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .contentType("images")
                    .build();
            s3Client.putObject(request, RequestBody.fromBytes(data));
        } catch (S3Exception e){
            throw new RuntimeException("Failed to upload image"+e.getMessage());
        }
    }

์ด๋ ‡๊ฒŒ ๋ฒ„์ผ“๊ณผ ์—ฐ๊ด€๋˜๋Š” ๊ธฐ๋Šฅ์„ ๋‹ค ์ •์˜ํ•ด์•ผํ•œ๋‹ค. ์• ์ดˆ์— s3client๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด service์ธต์—์„œ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ.

์—๋Ÿฌ

image service

์—๋Ÿฌ๋ฌธ

1

ava.lang.RuntimeException: Failed to upload imageThe request signature we calculated does not match the signature you provided. Check your secret access key and signing method. (Service: S3, Status Code: 403, Request ID: null) (SDK Attempt Count: 1)

2

=== Upload Debug Info === 
Bucket: temp 
Key: 2505241824089439733_1.png 
Data length: 663524 
=== Error Details === 
Status Code: 403 
Error Code: SignatureDoesNotMatch 
Error Message: The request signature we calculated does not match the signature you provided. Check your secret access key and signing method. 
Request ID: null

์„ค๋ช…

1

public void uploadImage(String bucket, String key, byte[] data){
        System.out.println("=== Upload Debug Info ===");
        System.out.println("Bucket: " + bucket);
        System.out.println("Key: " + key);
        System.out.println("Data length: " + data.length);
        try{
            PutObjectRequest request = PutObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();
            s3Client.putObject(request, RequestBody.fromBytes(data));
        } catch (S3Exception e){
            System.out.println("=== Error Details ===");
            System.out.println("Status Code: " + e.statusCode());
            System.out.println("Error Code: " + e.awsErrorDetails().errorCode());
            System.out.println(
	            "Error Message: " + e.awsErrorDetails().errorMessage()
            );
            System.out.println("Request ID: " + e.requestId());
            throw new RuntimeException("Failed to upload image"+e.getMessage());
        }
    }

์—ฌ๊ธฐ์„œ S3Exception์ด ๋ฐœ์ƒํ–ˆ๋˜๊ฒƒ.

๊ทธ๋ž˜์„œ api (account) ํ† ํฐ๋„ ๋‘์„ธ๋ฒˆ ๋‹ค์‹œ ๋ฐ›์•„๋ดค๋Š”๋ฐ๋„ ํ•ด๊ฒฐ์ด ์•ˆ๋๋‹ค. ๋ถ„๋ช…ํžˆ ๊ถŒํ•œ๋„ admin read & write๋กœ ์คฌ๋Š”๋ฐ.

System.out.println(r2Client.listBuckets());๋ฅผ ํ•˜๋ฉด ๋ฒ„ํ‚ท๋“ค ๋ชฉ๋ก์ด ์ž˜ ๋“ฑ์žฅํ–ˆ๋Š”๋ฐ. ์™œ ์—…๋กœ๋“œ ํ• ๋•Œ๋งŒ ์•ˆ๋˜์ง€?

ํด๋กœ๋“œ๊ฐ€ ์‹œํ‚ค๋Š”๋Œ€๋กœ PreSigned URL์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•ด๋ดค๋‹ค. ๊ณต์‹๋ฌธ์„œ์—์„œ๋Š”

2

public class CloudflareR2Client {
  private final S3Client s3Client;
  private final S3Presigner presigner;

    /**
     * Creates a new CloudflareR2Client with the provided configuration
     */
    public CloudflareR2Client(S3Config config) {
        this.s3Client = buildS3Client(config);
        this.presigner = buildS3Presigner(config);
    }

    /**
     * Builds and configures the S3 presigner with R2-specific settings
     */
    private static S3Presigner buildS3Presigner(S3Config config) {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(
            config.getAccessKey(),
            config.getSecretKey()
        );

        return S3Presigner.builder()
            .endpointOverride(URI.create(config.getEndpoint()))
            .credentialsProvider(StaticCredentialsProvider.create(credentials))
            .region(Region.of("auto"))
            .serviceConfiguration(S3Configuration.builder()
                .pathStyleAccessEnabled(true)
                .build())
            .build();
    }

    public String generatePresignedUploadUrl(
	    String bucketName, 
	    String objectKey, 
	    Duration expiration) {
        PutObjectPresignRequest presignRequest =
	        PutObjectPresignRequest.builder()
	            .signatureDuration(expiration)
	            .putObjectRequest(builder -> builder
	                .bucket(bucketName)
	                .key(objectKey)
	                .build())
	            .build();

        PresignedPutObjectRequest presignedRequest =
	        presigner.presignPutObject(presignRequest);
        return presignedRequest.url().toString();
    }

    // Rest of the methods remains the same
    public static void main(String[] args) {
      // config the client as before

      // Generate a pre-signed upload URL valid for 15 minutes
        String uploadUrl = r2Client.generatePresignedUploadUrl(
            "demos",
            "README.md",
            Duration.ofMinutes(15)
        );
        System.out.println("Pre-signed Upload URL (valid for 15 minutes):");
        System.out.println(uploadUrl);
    }
}

๊ทธ๋ž˜์„œ ๋‚˜๋Š”

@Configuration
public class S3Config {
    @Value("${cloudflare.r2.account.id}")
    private String accountId;
    @Value("${cloudflare.r2.access.key}")
    private String accessKey;
    @Value("${cloudflare.r2.secret.key}")
    private String secretKey;

    @Bean
    public S3Client buildS3Client(){
	    ...
    }

    @Bean
    public S3Presigner s3Presigner() {
        String endpoint = String.format("https://%s.r2.cloudflarestorage.com", accountId);

        return S3Presigner.builder()
                .endpointOverride(URI.create(endpoint))
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(accessKey, secretKey)))
                .region(Region.of("auto"))
                .build();
    }

}

์ด๋ ‡๊ฒŒ ๋ฐ‘์— S3Presigner๋งŒ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค. ๊ทธ๋ฆฌ๊ณ  r2Client ํด๋ž˜์Šค์— ๋ฉ”์†Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

public String generateUploadUrl(String bucket, String key) {
        try {
            PutObjectRequest request = PutObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            PutObjectPresignRequest presignRequest = 
	            PutObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(10))
                    .putObjectRequest(request)
                    .build();

            String url = 
	            s3Presigner.presignPutObject(presignRequest).url().toString();
            System.out.println("Generated PreSigned URL: " + url);
            return url;
        } catch (Exception e) {
            throw new RuntimeException(
	            "Failed to generate presigned URL: " + e.getMessage(), e);
        }
    }

๊ทธ๋ฆฌ๊ณ ๋‚˜์„œ

String presignedUrl = r2Client.generateUploadUrl("temp", "presigned-test.txt");
System.out.println("Use this URL to upload: " + presignedUrl);

์ด๊ฑธ ํ•ด๋ดค๋”๋‹ˆ

Generated PreSigned URL: [https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d](https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d) Use this URL to upload: [https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d](https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d)

์ด๋ ‡๊ฒŒ ๊ธด ์ฃผ์†Œ๊ฐ€ ๋ฐ˜ํ™˜๋๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฒ„ํ‚ท์„ ํ™•์ธํ•ด๋ณด๋‹ˆ presigned-test.txt๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๋‹ค.

ํ„ฐ๋ฏธ๋„์„ ์ผœ์„œ curl -X "<์œ„์˜์ฃผ์†Œ>" --data "Hello World Test"๋ฅผ ํ•ด๋ณด๋‹ˆ ๋น„์–ด์žˆ๋˜ ํ…์ŠคํŠธํŒŒ์ผ์— ํ—ฌ๋กœ์›”๋“œ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜”๋‹ค.

3

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ presigned url์„ ํ™œ์šฉํ•˜๋‹ˆ๊นŒ ์ œ๋Œ€๋กœ ์ž‘๋™์„ ํ•œ๊ฑด๋ฐ ์™ค๊นŒ? ํด๋กœ๋“œ๋Š” S3Config์˜ builder ๋ฉ”์„œ๋“œ์— ์žˆ๋Š” serviceConfig์— .chunkedEncodingEnabled(false)๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด๋ผ๊ณ  ํ–ˆ๋‹ค.

    @Bean
    public S3Client buildS3Client(){
        String endpoint = String.format(
        "https://%s.r2.cloudflarestorage.com", accountId);

        AwsBasicCredentials credentials = AwsBasicCredentials.create(
                accessKey, secretKey
        );

        S3Configuration serviceConfig = S3Configuration.builder()
                .pathStyleAccessEnabled(true)
                .chunkedEncodingEnabled(false)  // ์ถ”๊ฐ€
                .build();

        return S3Client.builder()
                .endpointOverride(URI.create(endpoint))
                .credentialsProvider(
	                StaticCredentialsProvider.create(credentials))
                .region(Region.of("auto"))
                .serviceConfiguration(serviceConfig)
                .build();
    }

๊ทธ๋žฌ๋”๋‹ˆ ์ด์ œ ์‚ฌ์ง„์ด ์˜ฌ๋ผ๊ฐ„๋‹ค.

์ •๋ฆฌ

  • r2Client.listBuckets()๊ฐ€ ์ž˜ ์ž‘๋™ํ•œ๋‹ค -> Credentials๊ฐ€ ์ž˜๋ชป ์ž…๋ ฅ๋œ๊ฑด ์•„๋‹ˆ๋ผ๋Š”๊ฒƒ์ด๋‹ค.
  • PreSigned url์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ  curl ๋ช…๋ น์–ด๋กœ ์—ฌ๊ธฐ์— ์—…๋กœ๋“œ๋„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ SDK๋ฅผ ํ†ตํ•ด์„œ putObject()๋งŒ ๊ณ„์† ์‹คํŒจํ•œ๋‹ค?

S3 ํ˜ธํ™˜ ์„œ๋น„์Šค๋“ค์„ AWS SDK๋กœ ์ด์šฉํ• ๋•Œ ํ”ํžˆ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์ ๋“ค

  • region ์„ค์ • ์ฐจ์ด
  • path style vs virtual hosted style
  • chunked transfer encoding ํ˜ธํ™˜์„ฑ
  • HTTP client ์„ค์ • ์ฐจ์ด